package algorithms.hint;

import simulator.Client;
import simulator.Manager;
import storage.Block;
import configuration.Configuration;
import java.util.LinkedHashMap;
import java.util.HashMap;
import java.util.Map;
import java.util.Collections;

/**
 * @author Nodirjon Siddikov
 */
public class HintClient extends Client {
	// local cache memory
	// key - local cache block ID (different than disk block ID)
	// value - local cache block content
	private LinkedHashMap<Integer, LocalCacheBlock> _lclCache; // client's local
																// cache
																// information
	// block location hints
	// key - disk block ID
	// value - disk block location hint
	private LinkedHashMap<Integer, Hint> _hints; // hints about locations of the
													// MASTER COPY
	// oldest block list - list of what the client believes to be
	// the age of of the oldest block in each client
	// key - client ID
	// value - oldest age of the local cache block in that client
	private HashMap<Integer, Long> _oldestBlockList;
	private HintManager _manager;
	private int _maxNoOfHints;

	// constructor to construct the client
	public HintClient(int id) {
		super(id);
		_lclCacheSize = Configuration.localCacheSize; // in Bytes
		// calculate the max number of cache blocks
		// that local cache memory can accommodate
		_maxNoOfCacheBlocks = _lclCacheSize / Configuration.localCacheBlockSize;
		_maxNoOfHints = Configuration.hintCount;
		// calculate the hashtable capacity
		int hashTableCapacity = (int) Math.ceil(_maxNoOfCacheBlocks
				/ _hashTableLoadFactor) + 1;
		// constructs linkedHashMap and its order of iteration is
		// from least accessed to most accessed (access-order)
		// access order is managed by linked list
		_lclCache = new LinkedHashMap<Integer, LocalCacheBlock>(
				hashTableCapacity, _hashTableLoadFactor, true) {
			private static final long serialVersionUID = 1L;

			// (an anonymous inner class)
			// this method is fired everytime put() method is called
			@Override
			protected boolean removeEldestEntry(
					Map.Entry<Integer, LocalCacheBlock> eldestEntry) {
				// size() - current LinkedHashMap size
				// _maxNoOfCacheBlocks - maximum allowed size
				if (size() > _maxNoOfCacheBlocks) {
					// this will handle what to do with to be removed cache
					// block
					handleRemovedCacheBlock(eldestEntry.getValue());
					// true = a cache block will be removed
					return true;
				}
				// false = cache block will NOT be removed
				return false;
			}
		};
		// instantiate the hints list
		// insertion-order
		_hints = new LinkedHashMap<Integer, Hint>() {
			private static final long serialVersionUID = 1L;

			// (an anonymous inner class)
			// this method is fired everytime put() method is called
			@Override
			protected boolean removeEldestEntry(
					Map.Entry<Integer, Hint> eldestEntry) {
				// check current size with initial size
				return size() > _maxNoOfHints;
			}
		};
		// instantiate the oldest block list
		_oldestBlockList = new HashMap<Integer, Long>();
		if (Configuration.detailedStats != 0) {
			System.out.println("Hint client " + this.getID() + " is up.");
			System.out
					.println("Local cache size: " + _lclCacheSize + " Bytes.");
			System.out.println("Max no. of local cache blocks: "
					+ _maxNoOfCacheBlocks);
			System.out.println("Max no. of hints: " + _maxNoOfHints);
		}
	}

	// set manager for the client
	public void setManager(Manager m) {
		_manager = (HintManager) m;
	}

	// receive a read request from an application
	public Block receiveReadRequest(int diskBlockID) {
		if (Configuration.detailedStats != 0) {
			System.out.println();
			System.out.println("Client " + getID() + " is requesting block "
					+ diskBlockID);
		}
		// STEP
		// 1:///////////////////////////////////////////////////////////////////////////
		_blckAccessTime = 0;
		_ttlNoOfReadRequests++;
		LocalCacheBlock lclCacheBlock = null;
		// search local cache memory
		for (Map.Entry<Integer, LocalCacheBlock> b : _lclCache.entrySet()) {
			if (b.getValue().getDiskBlockID() == diskBlockID) {
				// found
				lclCacheBlock = _lclCache.get(b.getKey());
				// stop looping and break
				break;
			}
		}
		_blckAccessTime += Configuration.cacheAccessTime;
		// if the block is cached locally
		if (lclCacheBlock != null) {
			_lclCacheHitCount++;
			_ttlBlockAccessTime += _blckAccessTime;
			if (Configuration.detailedStats != 0) {
				System.out.println("Local cache hit.");
				System.out.println("Block access time: " + _blckAccessTime);
			}
			// set the last access time
			lclCacheBlock.setLastAccessTimestamp(System.currentTimeMillis());
			// serve the disk block
			return lclCacheBlock.getDiskBlock();
		}
		// STEP
		// 2:////////////////////////////////////////////////////////////////////////////////////
		// increment the local cache miss
		_lclCacheMissCount++;
		if (Configuration.detailedStats != 0) {
			System.out.println("Local cache miss.");
		}
		// consult to its hits to
		// check if this client has hints for MASTER disk block location
		Hint lclHint = _hints.get(diskBlockID);
		_blckAccessTime += Configuration.cacheAccessTime;
		// if local hint is found
		if (lclHint != null) {
			// get reference to the remote client
			HintClient rmtClient = _manager.getClient(lclHint.getClientID());
			if (Configuration.detailedStats != 0) {
				System.out.println("Local hint: Master Copy disk block "
						+ diskBlockID + " is in client " + rmtClient.getID());
			}
			// contact to the client directly that has a MASTER disk block
			Block rmtDiskBlock = rmtClient.serveDiskBlock(diskBlockID);
			_blckAccessTime += (Configuration.messageLatency + Configuration.cacheAccessTime);
			if (rmtDiskBlock != null) {
				_rmtCacheHitCount++;
				_blockReadCount++;
				_ttlBlockAccessTime += _blckAccessTime;
				if (Configuration.detailedStats != 0) {
					System.out.println("Remote cache hit.");
					System.out.println("Disk block is fetched from client "
							+ rmtClient.getID());
					System.out.println("Block access time: " + _blckAccessTime);
				}
				boolean isMaster = false;
				cacheDiskBlock(rmtDiskBlock, isMaster);
				// exchange oldest blocks between clients
				exchangeOldestBlockTimestamp(rmtClient);
				return rmtDiskBlock;
			}
		}
		// STEP
		// 3://///////////////////////////////////////////////////////////////////////////////////
		// ask the manager for the hint
		Hint rmtHint = _manager.getHint(diskBlockID);
		_lookUpCount++;
		_hintExchangeCount++;
		_blckAccessTime += Configuration.messageLatency;
		// if the manager returned the hint
		if (rmtHint != null) {
			// add the fact from manager to local hints list
			_hints.put(diskBlockID, rmtHint);
			// get the reference to the remote client
			HintClient rmtClient = _manager.getClient(rmtHint.getClientID());
			if (Configuration.detailedStats != 0) {
				System.out.println("Manager's fact: Master Copy disk block "
						+ diskBlockID + " is in client " + rmtClient.getID());
			}
			// reference to the remote disk block
			Block rmtDiskBlock = rmtClient.serveDiskBlock(diskBlockID);
			_blckAccessTime += (Configuration.messageLatency + Configuration.cacheAccessTime);
			if (rmtDiskBlock != null) {
				_rmtCacheHitCount++;
				_blockReadCount++;
				_ttlBlockAccessTime += _blckAccessTime;
				if (Configuration.detailedStats != 0) {
					System.out.println("Remote cache hit.");
					System.out.println("Disk block " + rmtDiskBlock.getID()
							+ " is fetched from client " + rmtClient.getID());
					System.out.println("Block access time: " + _blckAccessTime);
				}
				// the block is being cached by other client, it is NOT a MASTER
				// BLOCK
				boolean isMaster = false;
				cacheDiskBlock(rmtDiskBlock, isMaster);
				// exchange oldest blocks between clients
				exchangeOldestBlockTimestamp(rmtClient);
				return rmtDiskBlock;
			}
		}
		// STEP
		// 4:////////////////////////////////////////////////////////////////////////////////////
		// if manager did not return the hint
		// increment the remote cache miss
		_rmtCacheMissCount++;
		if (Configuration.detailedStats != 0) {
			System.out.println("Remote cache miss.");
		}
		// ask manager to read the disk block from the disk
		Block rmtDiskBlock = _manager.readFromStorage(diskBlockID);
		_lookUpCount++;
		_blckAccessTime += (2 * Configuration.messageLatency + Configuration.storageAccessTime); // client
																									// ->
																									// mgr
																									// ->
																									// storage
		if (rmtDiskBlock != null) {
			_serverDiskHitCount++;
			_ttlBlockAccessTime += _blckAccessTime;
			if (Configuration.detailedStats != 0) {
				System.out.println("Block access time: " + _blckAccessTime);
			}
			// client is caching the disk block for the first time on entire
			// coop cache system
			// so the disk block is cached as MASTER copy
			boolean isMaster = true;
			cacheDiskBlock(rmtDiskBlock, isMaster);
			_manager.addFact(this.getID(), rmtDiskBlock.getID());
			_consistencyCount++;
			_hintExchangeCount++;
			return rmtDiskBlock;
		}
		return null;
	}

	// this method helps handling the removed cache block
	private void handleRemovedCacheBlock(LocalCacheBlock block) {
		// if the block is a MASTER COPY
		if (block.isMasterCopy()) {
			// forward the master copy to another client having the least
			// valuable block
			// in entire coop cache
			if (Configuration.detailedStats != 0) {
				System.out.println("Master copy disk block "
						+ block.getDiskBlockID() + " will be forwarded.");
			}
			forward(block.getDiskBlock());
		} else {
			// NON_MASTER copies are simply discarded
			if (Configuration.detailedStats != 0) {
				System.out.println("Non-master copy disk block "
						+ block.getDiskBlockID() + " is discarded.");
			}
		}
	}

	private Block serveDiskBlock(int diskBlockID) {
		// read the disk block from local cache memory
		// look up local cache
		for (LocalCacheBlock b : _lclCache.values()) {
			if (b.getDiskBlockID() == diskBlockID) {
				return b.getDiskBlock();
			}
		}
		return null;
	}

	// exchange the oldest disk block information
	// between TWO clients
	private void exchangeOldestBlockTimestamp(HintClient rmtClient) {
		// current client updates its list with oldest block timestamp from
		// remote client
		_oldestBlockList.put(rmtClient.getID(),
				rmtClient.getOldestBlockTimestamp());
		// the remote client also updates its oldest blocks list
		rmtClient.updateOldestBlockAge(this.getID(),
				this.getOldestBlockTimestamp());
	}

	// update oldest block age list
	// info comes from REMOTE client
	private void updateOldestBlockAge(int rmtClientID, long oldestBlockTimestamp) {
		_oldestBlockList.put(rmtClientID, oldestBlockTimestamp);
	}

	// get the oldest cache block age from current LCI
	private Long getOldestBlockTimestamp() {
		long oldestTimestamp = System.currentTimeMillis(); // get the current
															// timestamp as
															// oldest
		for (LocalCacheBlock b : _lclCache.values()) {
			// older cache blocks have SMALLER timestamp
			if (b.getLastAccessTimestamp() < oldestTimestamp) {
				// set the new oldest age
				oldestTimestamp = b.getLastAccessTimestamp();
			}
		}
		return oldestTimestamp;
	}

	// cache the disk block
	private void cacheDiskBlock(Block rmtDiskBlock, boolean isMaster) {
		// cache the disk block
		LocalCacheBlock lclCacheBlock = new LocalCacheBlock(new Block(
				rmtDiskBlock.getID(), rmtDiskBlock.getSize()), isMaster);
		// if this client already has NON-MASTER copy of the block
		for (Map.Entry<Integer, LocalCacheBlock> e : _lclCache.entrySet()) {
			if (e.getValue().getDiskBlockID() == rmtDiskBlock.getID()
					&& !e.getValue().isMasterCopy()) {
				// set as a master copy
				e.getValue().setAsMasterCopy();
				_lclCache.put(e.getKey(), e.getValue());
				return;
			}
		}
		// get the maximum cache block ID
		int newCacheBlockID = 0;
		if (!_lclCache.isEmpty()) {
			newCacheBlockID = Collections.max(_lclCache.keySet()) + 1;
		}
		// cache the local block
		_lclCache.put(newCacheBlockID, lclCacheBlock);
		if (Configuration.detailedStats != 0) {
			System.out.println("Client " + this.getID()
					+ " locally cached the disk block " + rmtDiskBlock.getID());
			printLocalCacheContent();
		}
	}

	private void printLocalCacheContent() {
		System.out.print("Client " + this.getID() + "'s local cache content: ");
		for (LocalCacheBlock b : _lclCache.values()) {
			String master = (b.isMasterCopy()) ? "m" : "";
			System.out.print(b.getDiskBlockID() + master + " ");
		}
		System.out.println();
	}

	// forward the disk block to other clients based on local age hints
	// only MASTER copies are forwarded
	private void forward(Block diskBlock) {
		// ID of the client with the oldest block
		int tgtClientID = -1;
		long oldestTimestamp = System.currentTimeMillis(); // get the current
															// time
		// iterate thru the oldest block list to find the ID of the client that
		// hosts the
		// oldest disk block
		// key - client ID
		// value - oldest age of the cache block in that client
		for (Map.Entry<Integer, Long> e : _oldestBlockList.entrySet()) {
			// oldest cache block has SMALLER timestamp value
			if (e.getValue() < oldestTimestamp) {
				// get new oldest age
				oldestTimestamp = e.getValue();
				tgtClientID = e.getKey();
			}
		}
		// get the reference of the client that has the oldest block
		HintClient tgtClient = _manager.getClient(tgtClientID);
		// if target client is found
		if (tgtClient != null) {
			if (Configuration.detailedStats != 0) {
				System.out.println("Master block " + diskBlock.getID()
						+ " will be forwarded to client " + tgtClient.getID());
			}
			tgtClient.makeSpaceForMasterBlock();
			// forwards only MASTER copies
			boolean isMaster = true;
			tgtClient.cacheDiskBlock(diskBlock, isMaster);
			_relocationCount++;
			// exchange oldest block ages between clients
			exchangeOldestBlockTimestamp(tgtClient);
			// both clients update their hints to reflect the
			// new location of the MASTER copy
			tgtClient.removeHint(diskBlock.getID());
			_hintExchangeCount++;
			this._hints.put(diskBlock.getID(), new Hint(diskBlock.getID(),
					tgtClientID));
			// update manager's facts list
			_manager.addFact(tgtClientID, diskBlock.getID());
			_consistencyCount++;
			_hintExchangeCount++;
		} else // if target client is NOT found
		{
			// simply discard the master block
			if (Configuration.detailedStats != 0) {
				System.out.println("No feasible client is found. Master block "
						+ diskBlock.getID() + " will be discarded.");
			}
		}
		// update the manager's fact list
		_manager.removeFact(this.getID(), diskBlock.getID());
		_consistencyCount++;
		_hintExchangeCount++;
	}

	// remove any hints related to this disk block
	private void removeHint(int diskBlockID) {
		_hints.remove(diskBlockID);
	}

	// make space for master disk block
	private void makeSpaceForMasterBlock() {
		if (_lclCache.size() >= _maxNoOfCacheBlocks) {
			// delete the disk block with oldest timestamp from target client
			// first
			for (Map.Entry<Integer, LocalCacheBlock> e : _lclCache.entrySet()) {
				// the first entry in the top is the oldest
				_lclCache.remove(e.getKey());
				if (e.getValue().isMasterCopy()) {
					// it gets removed from the manager
					_manager.removeFact(this.getID(), e.getValue()
							.getDiskBlockID());
					_consistencyCount++;
					_hintExchangeCount++;
					if (Configuration.detailedStats != 0) {
						System.out.println("Master copy disk block "
								+ e.getValue().getDiskBlockID()
								+ " is removed completely from client "
								+ this.getID()
								+ " to open space for master block.");
					}
				} else {
					if (Configuration.detailedStats != 0) {
						System.out.println("Non-master copy disk block "
								+ e.getValue().getDiskBlockID()
								+ " is removed completely from client "
								+ this.getID()
								+ " to open space for master block.");
					}
				}
				return;
			}
		}
	}

	// calculate the size of the coop cache size
	public int getClientCoopCacheSize() {
		// in bytes
		return _hints.size() * Configuration.hintSize;
	}

	// used for calculation of communication overhead for client (in and out of
	// Bytes)
	public double getDigestExchangeFraction() {
		return 0;
	}

	public double getBlockReadFraction() {
		return _blockReadCount * Configuration.blockSize;
	}

	public double getRelocationFraction() {
		return _relocationCount * Configuration.blockSize;
	}

	public double getHintExchangeFraction() {
		return _hintExchangeCount * Configuration.hintSize;
	}
}